package ezXpns.storage;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.UUID;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.RAMDirectory;

import ezXpns.analysis.Transform;

//TODO: optimize NRT reader
//TODO: handle exception

/**
 * Searcher enables fast searching of a Storage&ltT&gt. <br />
 * It achieves this by building an index of objects using Lucene. <br />
 * All searchers require an {@link ezXpns.analysis.Transform} which is in charge of converting an object of type T to
 * a Lucene document.
 * 
 * @param <T> Value type of the storage
 */

//@author A0088827L
class Searcher<T> implements AutoCloseable {
	
	/**
	 * Create a new searcher with the index kept in RAM 
	 * 
	 * @param storage the storage to build the searcher upon
	 * @param converter the converter used to convert stored objects to Lucene documents
	 * @throws IOException
	 */
	public Searcher(Storage<T> storage, Transform<Pair<UUID, T>, Document> converter) throws IOException {
		this(storage, converter, new RAMDirectory());
	}
	
	/**
	 * Create a new searcher with the index kept in the file system
	 * 
	 * @param storage the storage to build the searcher upon
	 * @param converter the converter used to convert stored objects to Lucene documents
	 * @param path the path to save the index to
	 * @throws IOException
	 */
	public Searcher(Storage<T> storage, Transform<Pair<UUID, T>, Document> converter, String path) throws IOException {
		this(storage, converter, FSDirectory.open(new File(path)));
	}
	
	/**
	 * Advanced constructor
	 * {@link Searcher}
	 * @param storage the storage to build the searcher upon
	 * @param converter the converter used to convert stored objects to Lucene documents
	 * @param directory the directory to use for the index
	 * @throws IOException
	 */
	public Searcher(Storage<T> storage, Transform<Pair<UUID, T>, Document> converter, Directory directory)
			throws IOException {
		
		indexer = new SearchIndexer<T>(directory, converter);
		storage.addListener(indexer);
		this.storage = storage;
		
		indexWriter = indexer.getIndexWriter();
		indexReader = DirectoryReader.open(indexWriter, true);
		searcher = new IndexSearcher(indexReader);
	}
	
	public void index(Entry<UUID, T> entry) {
		indexer.onAdd(entry.getKey(), entry.getValue());
	}

	/**
	 * Search using a Lucene string query
	 * 
	 * @param qs the query string
	 * @param batchSize size of a batch. This value should be at least as high as the number of results
	 * in a "page". If it is too high, more memory is consumed. If it is too low, performance may decrease.
	 * The value only affect performance and memory usage, not the result set.
	 * @return a stream of objects and their respective ids.
	 * @throws ParseException 
	 * @throws IOException
	 */
	public Iterable<Entry<UUID, T>> search(String qs, int batchSize) throws ParseException, IOException {
		org.apache.lucene.search.Query query = queryParser.parse(qs);
		return search(query, batchSize);
	}
	
	/**
	 * Search using a programmatically constructed ezXpns query
	 * 
	 * @param qs the query object
	 * @param batchSize size of a batch. This value should be at least as high as the number of results
	 * in a "page". If it is too high, more memory is consumed. If it is too low, performance may decrease.
	 * The value only affect performance and memory usage, not the result set.
	 * @return a stream of objects and their respective ids.
	 * @throws IOException
	 */	
	public Iterable<Entry<UUID, T>> search(ezXpns.storage.Query<T> query, int batchSize) throws IOException {
		return search(query.asLuceneQuery(), batchSize);
	}
	
	/**
	 * Search using a programmatically constructed Lucene query
	 * 
	 * @param qs the query object
	 * @param batchSize size of a batch. This value should be at least as high as the number of results
	 * in a "page". If it is too high, more memory is consumed. If it is too low, performance may decrease.
	 * The value only affect performance and memory usage, not the result set.
	 * @return a stream of objects and their respective ids.
	 * @throws IOException
	 */
	public Iterable<Entry<UUID, T>> search(org.apache.lucene.search.Query query, int batchSize) throws IOException {
		DirectoryReader newReader = DirectoryReader.openIfChanged(indexReader, indexWriter, true);
		boolean reopened = newReader != null;
		DirectoryReader reader = reopened ? newReader : indexReader;
		
		if(reopened) {
			searcher = new IndexSearcher(reader);
		}

		ScoreDoc[] docs = searcher.search(query, batchSize).scoreDocs;		
		Iterable<Entry<UUID, T>> resultStream = new ResultStream(docs, searcher, query, batchSize);
		
		if(reopened) {
			DirectoryReader oldReader = indexReader;
			indexReader = newReader;
			oldReader.close();
		}
		
		return resultStream;
	}

	@Override
	public void close() throws Exception {		
		if(indexer != null) {
			indexer.close();
			indexer = null;
		}
		
		if(indexReader != null) {
			indexReader.close();
			indexReader = null;
		}
	}
	
	private Storage<T> storage;
	private DirectoryReader indexReader;
	private IndexSearcher searcher;
	private IndexWriter indexWriter;
	private SearchIndexer<T> indexer;
	private QueryParser queryParser = new QueryParser(LuceneConfig.LUCENE_VERSION, "id", LuceneConfig.ANALYZER);
	
	private class ResultStream implements Iterable<Entry<UUID, T>> {

		public ResultStream(ScoreDoc[] docs, IndexSearcher searcher,
				org.apache.lucene.search.Query query, int batchSize) {
			this.docs = docs;
			this.searcher = searcher;
			this.query = query;
			this.batchSize = batchSize;
		}

		@Override
		public Iterator<Entry<UUID, T>> iterator() {
			return new ResultIterator();
		}
		
		private ScoreDoc[] docs;
		private IndexSearcher searcher;
		private org.apache.lucene.search.Query query;
		private int batchSize;
		
		private class ResultIterator implements Iterator<Entry<UUID, T>> {
			public ResultIterator() {
				index = 0;
				canContinue = true;
				currentEntry = fetchEntry();
			}
			
			@Override
			public boolean hasNext() {
				return canContinue;
			}

			@Override
			public Entry<UUID, T> next() {
				if(!canContinue) {
					throw new NoSuchElementException();
				}
				
				Entry<UUID, T> entry = currentEntry;
				currentEntry = fetchEntry();
				
				return entry;
			}
			
			@Override
			public void remove() {
				throw new UnsupportedOperationException("Cannot remove from a stream");
			}
			
			private Entry<UUID, T> fetchEntry() {
				if(index >= docs.length) {
					canContinue = false;
					return null;
				}
				
				ScoreDoc scoreDoc = docs[index++];
				Entry<UUID, T> result = null;
				try {
					Document document = searcher.doc(scoreDoc.doc);
					UUID id = UUID.fromString(document.getField("id").stringValue());
					T obj = storage.get(id);
					result = new ImmutablePair<UUID, T>(id, obj);
				} catch (IOException e) {
					// TODO Log
					e.printStackTrace();
					canContinue = false;
				}					
				
				//Fetch next batch if needed
				if(index >= docs.length) {
					try {
						docs = searcher.searchAfter(docs[docs.length - 1], query, batchSize).scoreDocs;
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}				
				}
				
				return result;
			}
			
			private boolean canContinue;
			private int index;
			private Entry<UUID, T> currentEntry;
		};
	}
}
